// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from './assert_ts.js';
import {PromiseResolver} from './promise_resolver.js';

export interface WebUiListener {
  eventName: string;
  uid: number;
}

/** Counter for use with createUid */
let uidCounter: number = 1;

/** @return A new unique ID. */
function createUid(): number {
  return uidCounter++;
}

/**
 * The mapping used by the sendWithPromise mechanism to tie the Promise
 * returned to callers with the corresponding WebUI response. The mapping is
 * from ID to the PromiseResolver helper; the ID is generated by
 * sendWithPromise and is unique across all invocations of said method.
 */
const chromeSendResolverMap: {[id: string]: PromiseResolver<any>} = {};

/**
 * The named method the WebUI handler calls directly in response to a
 * chrome.send call that expects a response. The handler requires no knowledge
 * of the specific name of this method, as the name is passed to the handler
 * as the first argument in the arguments list of chrome.send. The handler
 * must pass the ID, also sent via the chrome.send arguments list, as the
 * first argument of the JS invocation; additionally, the handler may
 * supply any number of other arguments that will be included in the response.
 * @param id The unique ID identifying the Promise this response is
 *     tied to.
 * @param isSuccess Whether the request was successful.
 * @param response The response as sent from C++.
 */
export function webUIResponse(id: string, isSuccess: boolean, response: any) {
  const resolver = chromeSendResolverMap[id];
  assert(resolver);
  delete chromeSendResolverMap[id];

  if (isSuccess) {
    resolver.resolve(response);
  } else {
    resolver.reject(response);
  }
}

/**
 * A variation of chrome.send, suitable for messages that expect a single
 * response from C++.
 * @param methodName The name of the WebUI handler API.
 * @param args Variable number of arguments to be forwarded to the
 *     C++ call.
 */
export function sendWithPromise(
    methodName: string, ...args: any[]): Promise<any> {
  const promiseResolver = new PromiseResolver();
  const id = methodName + '_' + createUid();
  chromeSendResolverMap[id] = promiseResolver;
  chrome.send(methodName, [id].concat(args));
  return promiseResolver.promise;
}

/**
 * A map of maps associating event names with listeners. The 2nd level map
 * associates a listener ID with the callback function, such that individual
 * listeners can be removed from an event without affecting other listeners of
 * the same event.
 */
const webUiListenerMap:
    {[event: string]: {[listenerId: number]: Function}} = {};

/**
 * The named method the WebUI handler calls directly when an event occurs.
 * The WebUI handler must supply the name of the event as the first argument
 * of the JS invocation; additionally, the handler may supply any number of
 * other arguments that will be forwarded to the listener callbacks.
 * @param event The name of the event that has occurred.
 * @param args Additional arguments passed from C++.
 */
export function webUIListenerCallback(event: string, ...args: any[]) {
  const eventListenersMap = webUiListenerMap[event];
  if (!eventListenersMap) {
    // C++ event sent for an event that has no listeners.
    // TODO(dpapad): Should a warning be displayed here?
    return;
  }

  for (const listenerId in eventListenersMap) {
    eventListenersMap[listenerId]!.apply(null, args);
  }
}

/**
 * Registers a listener for an event fired from WebUI handlers. Any number of
 * listeners may register for a single event.
 * @param eventName The event to listen to.
 * @param callback The callback run when the event is fired.
 * @return An object to be used for removing a listener via
 *     removeWebUiListener. Should be treated as read-only.
 */
export function addWebUiListener(
    eventName: string, callback: Function): WebUiListener {
  webUiListenerMap[eventName] = webUiListenerMap[eventName] || {};
  const uid = createUid();
  webUiListenerMap[eventName]![uid] = callback;
  return {eventName: eventName, uid: uid};
}

/**
 * Removes a listener. Does nothing if the specified listener is not found.
 * @param listener The listener to be removed (as returned by addWebUiListener).
 * @return Whether the given listener was found and actually removed.
 */
export function removeWebUiListener(listener: WebUiListener): boolean {
  const listenerExists = webUiListenerMap[listener.eventName] &&
      webUiListenerMap[listener.eventName]![listener.uid];
  if (listenerExists) {
    const map = webUiListenerMap[listener.eventName]!;
    delete map[listener.uid];
    return true;
  }
  return false;
}

// Globally expose functions that must be called from C++.
type WindowAndCr = Window&{cr?: object};
assert(!(window as WindowAndCr).cr);
Object.assign(window, {cr: {webUIResponse, webUIListenerCallback}});
